package com.gateway.router.gray;

import com.gateway.common.enums.GrayTypeEnum;
import com.gateway.common.utils.JsonUtils;
import com.gateway.plugin.base.condition.data.ParameterDataFactory;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Objects;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

/**
 * @program: api-gateway
 * @description:
 * @author: YuKai Fan
 * @create: 2025/2/20 15:32
 **/
public class GrayRouterGatewayFilterFactory extends AbstractGatewayFilterFactory<GrayRouterGatewayFilterFactory.GrayRouteFilterConfig> {

    private static final Logger log = LoggerFactory.getLogger(GrayRouterGatewayFilterFactory.class);
    private final DiscoveryClient discoveryClient;
    private LoadBalancerClientFactory loadBalancerClientFactory;

    public GrayRouterGatewayFilterFactory(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    public GrayRouterGatewayFilterFactory(DiscoveryClient discoveryClient, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.discoveryClient = discoveryClient;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
    }

    @Override
    public Class<GrayRouteFilterConfig> getConfigClass() {
        return GrayRouteFilterConfig.class;
    }

    @Override
    public GatewayFilter apply(GrayRouteFilterConfig grayRouteFilterConfig) {
        return (exchange, chain) -> {
            Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            Config config = grayRouteFilterConfig.getConfig();
            if (Objects.isNull(route) || !config.isEnabled()) {
                return chain.filter(exchange);
            }
            boolean isGray = shouldRouteToGray(exchange, config);
            // 修改路由目标
            if (isGray) {
                return chooseGrayInstance(route.getId(), config)
                        .flatMap(grayInstance -> {
                            if (Objects.isNull(grayInstance)) {
                                log.warn("No gray instance available, fallback to stable");
                            } else {
                                Route newRoute = Route.async()
                                        .id(route.getId())
                                        .uri(grayInstance.getUri())
                                        .asyncPredicate(route.getPredicate())
                                        .filters(route.getFilters())
                                        .build();
                                exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
                                log.info("Routed to gray instance : {}", grayInstance.getUri());
                            }
                            return chain.filter(exchange);
                        });
            } else {
                if (GrayTypeEnum.TAG.getType().equals(config.getGrayType())) {
                    // 选择不是灰度实例
                    return chooseNotGrayInstance(route.getId(), config)
                            .flatMap(notGrayInstance -> {
                                if (Objects.isNull(notGrayInstance)) {
                                    log.warn("No instance available, fallback to stable");
                                } else {
                                    Route newRoute = Route.async()
                                            .id(route.getId())
                                            .uri(notGrayInstance.getUri())
                                            .asyncPredicate(route.getPredicate())
                                            .filters(route.getFilters())
                                            .build();
                                    exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
                                    log.info("Routed to not gray instance : {}", notGrayInstance.getUri());
                                }
                                return chain.filter(exchange);
                            });
                }
                log.info("Routed to stable version for route: {}", route.getId());
            }
            return chain.filter(exchange);
        };
    }

    private boolean shouldRouteToGray(ServerWebExchange exchange, Config config) {
        String grayType = config.getGrayType();
        if (GrayTypeEnum.FLOW.getType().equals(grayType)) {
            if (config.getPercentage() > 0) {
                return Math.random() < config.getPercentage();
            }
        }
        if (GrayTypeEnum.TAG.getType().equals(grayType)) {
            List<Tag> tags = config.getTags();
            if (CollectionUtils.isNotEmpty(tags)) {
                tags = tags
                        .stream().filter(tag -> CollectionUtils.isNotEmpty(tag.getValues()))
                        .toList();
                if (CollectionUtils.isNotEmpty(tags)) {
                    for (Tag tag : tags) {
                        String data = ParameterDataFactory.builderData(tag.getParamType(), tag.getParamName(), exchange);
                        if (tag.getValues().contains(data)) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * 从实例元数据中 选择灰度实例
     *
     * @param serviceId
     * @param config
     * @return
     */
    private Mono<ServiceInstance> chooseGrayInstance(String serviceId, Config config) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        if (CollectionUtils.isEmpty(instances)) {
            return Mono.empty();
        }
        List<ServiceInstance> grayInstances = instances.stream()
                .filter(instance -> StringUtils.equals(config.getGrayVal(), instance.getMetadata().getOrDefault(config.getGrayParam(), "false")))
                .toList();
        if (CollectionUtils.isEmpty(grayInstances)) {
            // 说明没有灰度实例，直接返回一个稳定实例
            return Mono.just(instances.get(0));
        }
        if (config.isLoadbalance() && loadBalancerClientFactory != null) {
            ReactorLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
                    .getInstance(serviceId, ReactorLoadBalancer.class, ServiceInstanceListSupplier.class);
            return loadBalancer.choose(new DefaultRequest<>(grayInstances))
                    .map(response -> response.hasServer() ? response.getServer() : null);
        }
        // 默认返回第一个实例
        return Mono.just(grayInstances.get(0));
    }

    /**
     * 从实例元数据中 选择灰度实例
     *
     * @param serviceId
     * @param config
     * @return
     */
    private Mono<ServiceInstance> chooseNotGrayInstance(String serviceId, Config config) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        if (CollectionUtils.isEmpty(instances)) {
            return Mono.empty();
        }
        List<ServiceInstance> notGrayInstances = instances.stream()
                .filter(instance -> !StringUtils.equals(config.getGrayVal(), instance.getMetadata().getOrDefault(config.getGrayParam(), "false")))
                .toList();
        if (CollectionUtils.isEmpty(notGrayInstances)) {
            // 说明没有灰度实例，直接返回一个稳定实例
            return Mono.just(instances.get(0));
        }
        if (config.isLoadbalance() && loadBalancerClientFactory != null) {
            ReactorLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
                    .getInstance(serviceId, ReactorLoadBalancer.class, ServiceInstanceListSupplier.class);
            return loadBalancer.choose(new DefaultRequest<>(notGrayInstances))
                    .map(response -> response.hasServer() ? response.getServer() : null);
        }
        // 默认返回第一个实例
        return Mono.just(notGrayInstances.get(0));
    }

    @Override
    public String name() {
        return "GrayRouter";
    }

    public static class GrayRouteFilterConfig {

        private String config;

        public Config getConfig() {
            return StringUtils.isNotEmpty(config) ? JsonUtils.stringToBean(config, Config.class) : new Config();
        }

        public void setConfig(String config) {
            this.config = config;
        }
    }


    public static class Config {

        /**
         * 是否启用灰度
         */
        private boolean enabled = false;

        /**
         * 根据灰度类型
         * 1. flow 流量
         * 2. tag 标签
         */
        private String grayType = "flow";

        /**
         * 灰度参数
         */
        private String grayParam;

        /**
         * 灰度值
         */
        private String grayVal;

        /**
         * 灰度流量百分比
         */
        private double percentage = 0.0;

        /**
         * 是否开启负载均衡
         */
        private boolean loadbalance = false;

        /**
         * 数据转换
         */
        private List<Tag> tags;

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public double getPercentage() {
            return percentage;
        }

        public void setPercentage(double percentage) {
            this.percentage = percentage;
        }

        public String getGrayParam() {
            return grayParam;
        }

        public void setGrayParam(String grayParam) {
            this.grayParam = grayParam;
        }

        public String getGrayVal() {
            return grayVal;
        }

        public void setGrayVal(String grayVal) {
            this.grayVal = grayVal;
        }

        public boolean isLoadbalance() {
            return loadbalance;
        }

        public void setLoadbalance(boolean loadbalance) {
            this.loadbalance = loadbalance;
        }

        public String getGrayType() {
            return grayType;
        }

        public void setGrayType(String grayType) {
            this.grayType = grayType;
        }

        public List<Tag> getTags() {
            return tags;
        }

        public void setTags(List<Tag> tags) {
            this.tags = tags;
        }
    }

    public static class Tag {

        private String paramType;

        private String paramName;

        private List<String> values;

        public String getParamType() {
            return paramType;
        }

        public void setParamType(String paramType) {
            this.paramType = paramType;
        }

        public String getParamName() {
            return paramName;
        }

        public void setParamName(String paramName) {
            this.paramName = paramName;
        }

        public List<String> getValues() {
            return values;
        }

        public void setValues(List<String> values) {
            this.values = values;
        }
    }
}
